/**
 * Aptana Studio
 * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
 * Please see the license.html included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
package com.aptana.editor.common.scripting.snippets;

import java.text.MessageFormat;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension6;
import org.eclipse.jface.text.link.ILinkedModeListener;
import org.eclipse.jface.text.link.InclusivePositionUpdater;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.link.ProposalPosition;
import org.eclipse.jface.text.templates.GlobalTemplateVariables;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateBuffer;
import org.eclipse.jface.text.templates.TemplateContext;
import org.eclipse.jface.text.templates.TemplateException;
import org.eclipse.jface.text.templates.TemplateProposal;
import org.eclipse.jface.text.templates.TemplateVariable;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Shell;

import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.StringUtil;
import com.aptana.editor.common.CommonEditorPlugin;
import com.aptana.editor.common.contentassist.ICommonCompletionProposal;
import com.aptana.editor.common.util.EditorUtil;
import com.aptana.ui.epl.scripting.snippets.PositionBasedCompletionProposal;

public class SnippetTemplateProposal extends TemplateProposal implements ICommonCompletionProposal,
		ICompletionProposalExtension6, Comparable<ICompletionProposal>
{
	protected ICompletionProposal[] templateProposals;
	protected char triggerChar;
	protected char[] triggerChars;

	private final Template fTemplate;
	private final TemplateContext fContext;
	private final IRegion fRegion;
	private int fRelevance;

	private ICompletionProposal delegateTemplateProposal;
	private IRegion fSelectedRegion; // initialized by apply()

	private InclusivePositionUpdater fUpdater;

	private StyledString styledDisplayString;
	private StyledString styledActivationString;

	private Styler styler;

	/**
	 * Cached value for replacement offset.
	 */
	private Integer fReplaceOffset;

	public SnippetTemplateProposal(Template template, TemplateContext context, IRegion region, Image image,
			int relevance)
	{
		super(template, context, region, image, relevance);

		fTemplate = template;
		fContext = context;
		fRegion = region;
		fTemplate.getContextTypeId();
		EditorUtil.getSpaceIndentSize();

	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.jface.text.templates.TemplateProposal#getAdditionalProposalInfo()
	 */
	public String getAdditionalProposalInfo()
	{
		return getTemplate().getPattern();
	}

	@Override
	public void apply(final ITextViewer viewer, char trigger, int stateMask, final int offset)
	{
		delegateTemplateProposal = null;
		if (contains(triggerChars, trigger))
		{
			if (triggerChar == trigger)
			{
				doApply(viewer, trigger, stateMask, offset);
			}
			else
			{
				delegateTemplateProposal = templateProposals[trigger - '1'];
				((ICompletionProposalExtension2) templateProposals[trigger - '1']).apply(viewer, trigger, stateMask,
						offset);
			}
		}
		else
		{
			doApply(viewer, trigger, stateMask, offset);
		}
	}

	protected void doApply(final ITextViewer viewer, char trigger, int stateMask, final int offset)
	{
		IDocument document = viewer.getDocument();

		try
		{
			fContext.setReadOnly(false);
			int start;
			TemplateBuffer templateBuffer;
			{
				int oldReplaceOffset = getReplaceOffset(document, fTemplate);

				if (fTemplate instanceof SnippetTemplate)
				{
					// Reset indented pattern
					((SnippetTemplate) fTemplate).setIndentedPattern(null);

					IRegion lineInformationOfStart = document.getLineInformationOfOffset(oldReplaceOffset);
					int lineInformationOfStartOffset = lineInformationOfStart.getOffset();
					if (oldReplaceOffset > lineInformationOfStartOffset)
					{
						// Get the text from beginning of line to the replacement start offset
						String prefix = document.get(lineInformationOfStartOffset, oldReplaceOffset
								- lineInformationOfStartOffset);

						// Is there any leading white space?
						if (prefix.matches("\\s+.*")) //$NON-NLS-1$
						{
							// Yes. Prefix each line in the template's pattern with the same white space
							String indentedPattern = fTemplate.getPattern().replaceAll("(\r\n|\r|\n)", //$NON-NLS-1$
									"$1" + prefix.replaceFirst("(\\s+).*", "$1")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

							// We need to convert so that the pattern uses the same type of indent (spaces vs tabs)
							indentedPattern = conformIndents(prefix, indentedPattern);

							((SnippetTemplate) fTemplate).setIndentedPattern(indentedPattern);
						}
					}
				}

				try
				{
					// this may already modify the document
					templateBuffer = fContext.evaluate(fTemplate);
				}
				catch (TemplateException e)
				{
					IdeLog.logWarning(CommonEditorPlugin.getDefault(),
							MessageFormat.format("Error in template {0}. {1}", fTemplate.toString(), e.getMessage())); //$NON-NLS-1$
					fSelectedRegion = fRegion;
					return;
				}

				start = getReplaceOffset(document, fTemplate);
				int shift = start - oldReplaceOffset;
				int end = Math.max(getReplaceEndOffset(), offset + shift);

				// insert template string
				String templateString = templateBuffer.getString();
				document.replace(start, end - start, templateString);
			}

			// translate positions
			LinkedModeModel model = new LinkedModeModel();
			TemplateVariable[] variables = templateBuffer.getVariables();

			// If there is no tab stop variable in this template use
			// default sequence number
			int defaultSequenceNumber = LinkedPositionGroup.NO_STOP;
			for (TemplateVariable templateVariable : variables)
			{
				String type = templateVariable.getType();
				if (TabStopVariableResolver.VARIABLE_TYPE.equals(type))
				{
					try
					{
						Integer.parseInt(templateVariable.getName());
						// The non tab stop variables are visited after
						// visiting the tab stop variables
						defaultSequenceNumber = Integer.MAX_VALUE - 1;
						break;
					}
					catch (NumberFormatException nfe)
					{
						// ignore
					}
				}
			}

			boolean hasPositions = false;
			for (int i = 0; i != variables.length; i++)
			{
				TemplateVariable variable = variables[i];

				if (variable.isUnambiguous())
					continue;

				LinkedPositionGroup group = new LinkedPositionGroup();

				int[] offsets = variable.getOffsets();
				int length = variable.getLength();

				int sequenceNumber = defaultSequenceNumber;

				String type = variable.getType();
				if (TabStopVariableResolver.VARIABLE_TYPE.equals(type))
				{
					try
					{
						sequenceNumber = Integer.parseInt(variable.getName());
					}
					catch (NumberFormatException nfe)
					{
						// ignore
					}
				}

				LinkedPosition first;
				{
					String[] values = variable.getValues();
					ICompletionProposal[] proposals = new ICompletionProposal[values.length];
					for (int j = 0; j < values.length; j++)
					{
						ensurePositionCategoryInstalled(document, model);
						Position pos = new Position(offsets[0] + start, length);
						document.addPosition(getCategory(), pos);
						proposals[j] = new PositionBasedCompletionProposal(values[j], pos, length);
					}

					if (proposals.length > 1)
					{
						first = new ProposalPosition(document, offsets[0] + start, length, sequenceNumber, proposals);
					}
					else
					{
						first = new LinkedPosition(document, offsets[0] + start, length, sequenceNumber);
					}
				}

				for (int j = 0; j != offsets.length; j++)
				{
					if (j == 0)
					{
						group.addPosition(first);
					}
					else
					{
						group.addPosition(new LinkedPosition(document, offsets[j] + start, length));
					}
				}

				model.addGroup(group);
				hasPositions = true;
			}

			if (hasPositions)
			{
				model.forceInstall();
				LinkedModeUI ui = new LinkedModeUI(model, viewer);

				// Do not cycle
				ui.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
				ui.setExitPosition(viewer, getCaretOffset(templateBuffer) + start, 0, Integer.MAX_VALUE);
				ui.enter();

				fSelectedRegion = ui.getSelectedRegion();
			}
			else
			{
				ensurePositionCategoryRemoved(document);
				fSelectedRegion = new Region(getCaretOffset(templateBuffer) + start, 0);
			}
		}
		catch (BadLocationException e)
		{
			openErrorDialog(viewer.getTextWidget().getShell(), e);
			ensurePositionCategoryRemoved(document);
			fSelectedRegion = fRegion;
		}
		catch (BadPositionCategoryException e)
		{
			openErrorDialog(viewer.getTextWidget().getShell(), e);
			fSelectedRegion = fRegion;
		}

		if (fSelectedRegion == null)
		{
			fSelectedRegion = fRegion; // default case
		}
	}

	/*
	 * @see
	 * org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#validate(org.eclipse.jface.text.IDocument,
	 * int, org.eclipse.jface.text.DocumentEvent)
	 */
	public boolean validate(IDocument document, int offset, DocumentEvent event)
	{
		try
		{
			int replaceOffset = getReplaceOffset(document, fTemplate);
			if (offset >= replaceOffset)
			{
				String content = document.get(replaceOffset, offset - replaceOffset);
				return fTemplate.getName().startsWith(content);
			}
		}
		catch (BadLocationException e)
		{
			// concurrent modification - ignore
		}
		return false;
	}

	private synchronized int getReplaceOffset(IDocument document, Template template)
	{
		// Cache this value, so we don't need to compute it multiple times...
		if (fReplaceOffset == null)
		{
			if (template instanceof CommandTemplate)
			{
				try
				{
					CommandTemplate ct = (CommandTemplate) template;
					// Need to get correct offset based on prefix chopping!
					int fullPrefixOffset = getReplaceOffset();
					String prefix = document.get(fullPrefixOffset, getReplaceEndOffset() - fullPrefixOffset);
					final String origPrefix = prefix;
					while (!ct.matches(prefix))
					{
						prefix = SnippetsCompletionProcessor.narrowPrefix(prefix);
					}
					if (prefix.length() == 0)
					{
						fReplaceOffset = fullPrefixOffset;
					}
					else
					{
						fReplaceOffset = fullPrefixOffset + (origPrefix.length() - prefix.length());
					}
				}
				catch (BadLocationException e)
				{
					// ignore
					fReplaceOffset = getReplaceOffset();
				}
			}
			else
			{
				fReplaceOffset = getReplaceOffset();
			}
		}
		return fReplaceOffset;
	}

	/**
	 * Given the prefix text in the editor, modify the snippet patterns' indents to use same type of indentation (tabs
	 * vs spaces)
	 * 
	 * @param prefix
	 * @param indentedPattern
	 * @return
	 */
	protected String conformIndents(String prefix, String indentedPattern)
	{
		boolean useTabs = prefix.contains("\t"); //$NON-NLS-1$
		int indentSize = EditorUtil.getSpaceIndentSize();

		Pattern p = Pattern.compile("(\r\n|\r|\n)(\\s*)"); //$NON-NLS-1$
		Matcher m = p.matcher(indentedPattern);
		int startIndex = 0;
		StringBuilder builder = new StringBuilder();
		while (m.find(startIndex))
		{
			builder.append(indentedPattern.substring(startIndex, m.start(1)));
			startIndex = m.end(2);
			String indent = EditorUtil.convertIndent(m.group(2), indentSize, useTabs);
			builder.append(m.group(1));
			builder.append(indent);
		}
		builder.append(indentedPattern.substring(startIndex));
		indentedPattern = builder.toString();
		return indentedPattern;
	}

	private void ensurePositionCategoryInstalled(final IDocument document, LinkedModeModel model)
	{
		if (!document.containsPositionCategory(getCategory()))
		{
			document.addPositionCategory(getCategory());
			fUpdater = new InclusivePositionUpdater(getCategory());
			document.addPositionUpdater(fUpdater);

			model.addLinkingListener(new ILinkedModeListener()
			{
				/*
				 * @see
				 * org.eclipse.jface.text.link.ILinkedModeListener#left(org.eclipse.jface.text.link.LinkedModeModel,
				 * int)
				 */
				public void left(LinkedModeModel environment, int flags)
				{
					ensurePositionCategoryRemoved(document);
				}

				public void suspend(LinkedModeModel environment)
				{
				}

				public void resume(LinkedModeModel environment, int flags)
				{
				}
			});
		}
	}

	private void ensurePositionCategoryRemoved(IDocument document)
	{
		if (document.containsPositionCategory(getCategory()))
		{
			try
			{
				document.removePositionCategory(getCategory());
			}
			catch (BadPositionCategoryException e)
			{
				// ignore
			}
			document.removePositionUpdater(fUpdater);
		}
	}

	private String getCategory()
	{
		return "SnippetTemplateProposalCategory_" + toString(); //$NON-NLS-1$
	}

	private int getCaretOffset(TemplateBuffer buffer)
	{

		TemplateVariable[] variables = buffer.getVariables();
		for (int i = 0; i != variables.length; i++)
		{
			TemplateVariable variable = variables[i];
			if (variable.getType().equals(GlobalTemplateVariables.Cursor.NAME))
				return variable.getOffsets()[0];
		}

		return buffer.getString().length();
	}

	private void openErrorDialog(Shell shell, Exception e)
	{
		MessageDialog.openError(shell, Messages.SnippetTemplateProposal_TITLE_SnippetTemplateProposalError,
				e.getMessage());
	}

	/*
	 * @see ICompletionProposal#getSelection(IDocument)
	 */
	public Point getSelection(IDocument document)
	{
		if (delegateTemplateProposal == null)
		{
			return new Point(fSelectedRegion.getOffset(), fSelectedRegion.getLength());
		}
		return delegateTemplateProposal.getSelection(document);
	}

	@Override
	public char[] getTriggerCharacters()
	{
		return triggerChars;
	}

	@Override
	public String getDisplayString()
	{
		return getStyledDisplayString().getString().trim();
	}

	public String getActivationString()
	{
		return getStyledActivationString().getString().trim();
	}

	public StyledString getStyledDisplayString()
	{
		if (styledDisplayString == null)
		{
			Template template = getTemplate();
			styledDisplayString = new StyledString(template.getDescription(), styler);
		}
		return styledDisplayString;
	}

	public StyledString getStyledActivationString()
	{
		if (styledActivationString == null)
		{
			Template template = getTemplate();
			styledActivationString = new StyledString(String.format("%1$10.10s ", //$NON-NLS-1$
					template.getName() + " \u00bb") //$NON-NLS-1$
					+ ((triggerChar == '\000') ? " " : String.valueOf(triggerChar)) //$NON-NLS-1$
					// Need padding on windows to work around the width computation
					+ (Platform.OS_WIN32.equals(Platform.getOS()) ? "                                " : ""), //$NON-NLS-1$ //$NON-NLS-2$ 
					styler);
		}
		return styledActivationString;
	}

	Template getTemplateSuper()
	{
		return super.getTemplate();
	}

	public void setTriggerChar(char triggerChar)
	{
		this.triggerChar = triggerChar;
	}

	void setStyler(Styler styler)
	{
		this.styler = styler;
	}

	private static final String TRIGGER_CHARS = "123456789"; //$NON-NLS-1$

	void setTemplateProposals(ICompletionProposal[] templateProposals)
	{
		this.templateProposals = templateProposals;
		triggerChars = new char[Math.min(templateProposals.length, TRIGGER_CHARS.length())];
		TRIGGER_CHARS.getChars(0, triggerChars.length, triggerChars, 0);
	}

	protected static boolean contains(char[] characters, char c)
	{
		if (characters == null)
			return false;

		for (int i = 0; i < characters.length; i++)
		{
			if (c == characters[i])
				return true;
		}

		return false;
	}

	/*
	 * (non-Javadoc)
	 * @see com.aptana.editor.common.contentassist.ICommonCompletionProposal#getFileLocation()
	 */
	public String getFileLocation()
	{
		return getActivationString();
	}

	/*
	 * (non-Javadoc)
	 * @see com.aptana.editor.common.contentassist.ICommonCompletionProposal#getUserAgentImages()
	 */
	public Image[] getUserAgentImages()
	{
		return null;
	}

	/*
	 * (non-Javadoc)
	 * @see com.aptana.editor.common.contentassist.ICommonCompletionProposal#isDefaultSelection()
	 */
	public boolean isDefaultSelection()
	{
		return false;
	}

	/*
	 * (non-Javadoc)
	 * @see com.aptana.editor.common.contentassist.ICommonCompletionProposal#setIsSuggestedSelection()
	 */
	public boolean isSuggestedSelection()
	{
		return false;
	}

	/*
	 * (non-Javadoc)
	 * @see com.aptana.editor.common.contentassist.ICommonCompletionProposal#setIsSuggestedSelection()
	 */
	public void setIsDefaultSelection(boolean isDefault)
	{
	}

	/*
	 * (non-Javadoc)
	 * @see com.aptana.editor.common.contentassist.ICommonCompletionProposal#setIsSuggestedSelection()
	 */
	public void setIsSuggestedSelection(boolean isSuggested)
	{
	}

	/*
	 * (non-Javadoc)
	 * @see com.aptana.editor.common.contentassist.ICommonCompletionProposal#getExtraInfo()
	 */
	public String getExtraInfo()
	{
		return getActivationString();
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Comparable#compareTo(java.lang.Object)
	 */
	public int compareTo(ICompletionProposal o)
	{
		if (this == o)
		{
			return 0;
		}

		Template t = getTemplate();
		if (t == null)
		{
			return -1;
		}

		if (o instanceof SnippetTemplateProposal)
		{
			Template t2 = ((SnippetTemplateProposal) o).getTemplate();
			if (t2 == null)
			{
				return 1;
			}

			return StringUtil.compareCaseInsensitive(t.getName(), t2.getName());
		}
		else
		{
			return StringUtil.compareCaseInsensitive(t.getName(), o.getDisplayString());
		}
	}

	/*
	 * (non-Javadoc)
	 * @see com.aptana.editor.common.contentassist.ICommonCompletionProposal#getRelevance()
	 */
	public int getRelevance()
	{
		return fRelevance;
	}

	/*
	 * (non-Javadoc)
	 * @see com.aptana.editor.common.contentassist.ICommonCompletionProposal#setRelevance(int)
	 */
	public void setRelevance(int relevance)
	{
		fRelevance = relevance;
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * com.aptana.editor.common.contentassist.ICommonCompletionProposal#isTriggerEnabled(org.eclipse.jface.text.IDocument
	 * , int)
	 */
	public boolean validateTrigger(IDocument document, int offset, KeyEvent keyEvent)
	{
		try
		{
			int replaceOffset = getReplaceOffset(document, fTemplate);
			if (offset >= replaceOffset)
			{
				String content = document.get(replaceOffset, offset - replaceOffset);
				return fTemplate.getName().equals(content);
			}
		}
		catch (BadLocationException e)
		{
			// concurrent modification - ignore
		}
		return false;
	}

}
